package lelouet.sh3d.plugins.sweetDatacenter.model;

import gridlib.api.ConfigurationException;
import gridlib.api.Level;
import gridlib.api.Machine;
import gridlib.api.Rack;

import java.rmi.RemoteException;
import java.util.Iterator;
import java.util.NoSuchElementException;

import com.eteks.sweethome3d.model.PieceOfFurniture;

/**
 * a Rack is a physical Element that can contain servers. Servers are dispatched
 * among levels in the rack
 */
public class GRack extends GElement implements Iterable<GServer> {

	private static final long serialVersionUID = 1L;

	/**
	 * the size, in cm, that cannot be used to place a server because of the
	 * bottom border
	 */
	protected float border_bottom = 10;
	/**
	 * the size, in cm, that cannot be used to place a server because of the top
	 * border
	 */
	protected float border_top = 10;

	/** the number of level, or the height in U, of that rack */
	protected int lvlNb = 0;

	/** for each level, the servers that are on this level */
	protected GServer servers[];

	protected Rack gridElement;

	protected GRack(GElementFactory bridge, final PieceOfFurniture model,
			Rack rack) {
		super(bridge, model);
		gridElement = rack;
		lvlNb = (int) Math.floor((getHeight() - border_bottom - border_top)
				/ Utils.U_IN_CM);
		servers = new GServer[lvlNb];
	}

	public void setLvlNb(int nb) {
		adaptToNewSize(nb);
		setHeight(Utils.U_IN_CM * nb + border_bottom + border_top);
	}

	/**
	 * make the servers that are above specified size fall.<br />
	 * any remaining server above specified size is removed.
	 */
	protected void adaptToNewSize(int size) {
		if (size == getLvlNb()) {
			return;
		}
		if (size > getLvlNb()) { // dont recreate the array if it is already
			// long enough.
			if (size > servers.length) {
				GServer newServers[] = new GServer[size];
				for (int i = 0; i < getLvlNb(); i++) {
					newServers[i] = servers[i];
				}
				servers = newServers;
			}
		} else {
			// get the number of servers that are above the required size;
			int serversToMove = 0;
			for (int i = getLvlNb() - 1; i >= size; i--) {
				if (serverAtLvl(i) != null) {
					serversToMove++;
				}
			}
			// get the highest level we can start at to make the servers fall
			int hughestlevel = size - 1;
			for (; hughestlevel >= 0 && serversToMove > 0; hughestlevel--) {
				if (serverAtLvl(hughestlevel) == null) {
					serversToMove--;
					if (serversToMove == 0) {
						hughestlevel++;
					}
				}
			}
			int destination = hughestlevel;
			// then go back to the top, making the servers fall
			for (; destination < size; destination++) {
				if (serverAtLvl(destination) != null) {
					System.out.println("warning : server not empty at level "
							+ destination + " : " + serverAtLvl(destination));
				}
				int nextpos = destination;
				if (serverAtLvl(destination) != null) {
					nextpos += serverAtLvl(destination).getVSize();
				}
				for (; nextpos < lvlNb && serverAtLvl(nextpos) == null; nextpos++) {
				}
				if (nextpos < lvlNb) {
					GServer tomove = serverAtLvl(nextpos);
					unsetServer(tomove);
					forceServer(tomove, destination);
					destination += tomove.getVSize() - 1;
				}
			}
		}
		lvlNb = size;
	}

	public String describe(String mintab) {
		StringBuilder buff = new StringBuilder();
		buff.append(mintab);
		buff.append(getName());
		buff.append("\n");
		buff.append(mintab);
		buff.append(" " + getLvlNb() + " levels\n");
		String tab = mintab + "  ";
		GServer lastDone = null;
		for (int i = 0; i < lvlNb; i++) {
			GServer server = serverAtLvl(i);
			if (server != lastDone) {
				if (lastDone != null) {
					buff.append(i - 1);
					buff.append(" : ");
					buff.append(lastDone.getName());
					buff.append("\n");
				}
				if (server != null) {
					buff.append(tab);
					buff.append(i);
					buff.append(" - ");
				}
			}
			lastDone = server;
		}
		if (lastDone != null) {
			buff.append(getLvlNb() - 1);
			buff.append(" : ");
			buff.append(lastDone.getName());
			buff.append("\n");
		}
		return buff.toString();
	}

	/**
	 * try to put the server in that rack, close to its present location
	 * 
	 * @return -1 if could not add that server, the first level it was added on
	 *         success
	 */
	public int addServer(final GServer server) {
		final int idealLvl = Math.round((server.getElevation() - border_bottom)
				/ Utils.U_IN_CM);
		return this.addServer(server, idealLvl);
	}

	/**
	 * try to position a server on a level. If the rack can't add the server,
	 * then it tries first to put in the lower levels
	 * 
	 * @param idealLvl
	 *            the level that would be the best for the server
	 * @return the level the server has been added to, or -1 on failure
	 */
	public int addServer(final GServer server, final int idealLvl) {
		return addServer(server, idealLvl, false);
	}

	/**
	 * @param server
	 * @param lvl
	 * @param topFirst
	 *            should we try to put the server on the upper levels if we fail
	 *            fist?
	 * @return the level the server has been added to, or -1 on failure
	 */
	public int addServer(final GServer server, int lvl, final boolean topFirst) {
		if (lvl < 0) {
			lvl = 0;
		} else if (lvl >= getLvlNb()) {
			lvl = getLvlNb() - 1;
		}
		if (putServer(server, lvl)) {
			return lvl;
		}
		if (topFirst) {
			final int pos = sequentialPlaceServer(server, lvl + 1, 1);
			if (pos > -1) {
				return pos;
			}
		}
		{
			final int pos = sequentialPlaceServer(server, lvl - 1, -1);
			if (pos > -1) {
				return pos;
			}
		}
		if (!topFirst) {
			final int pos = sequentialPlaceServer(server, lvl + 1, 1);
			if (pos > -1) {
				return pos;
			}
		}
		return -1;
	}

	@Override
	public String getName() {
		try {
			return gridElement.getName();
		} catch (RemoteException e) {
			e.printStackTrace(System.err);
			return "";
		}
	}

	@Override
	public void setName(String name) {
		try {
			gridElement.setName(name);
		} catch (Exception e) {
			e.printStackTrace(System.err);
		}
	}

	/** Places the server on specified level. Not check is done. */
	protected void forceServer(final GServer server, final int lvl) {
		servers[lvl] = server;
		server.setOwner(this);
		this.updateServerPosition(server, lvl);
	}

	/**
	 * @return the number of level this rack has.<br />
	 *         One can put servers on levels 0..LvlNb-1
	 */
	public int getLvlNb() {
		return lvlNb;
	}

	@Override
	public Iterator<GServer> iterator() {
		return new Iterator<GServer>() {

			int nextPos = 0;
			GServer next = null;

			@Override
			public boolean hasNext() {
				if (next != null) {
					return true;
				}
				while (nextPos < getLvlNb()) {
					next = serverAtLvl(nextPos);
					if (next != null) {
						nextPos += next.getVSize();
						return true;
					}
					nextPos++;
				}
				return false;
			}

			@Override
			public GServer next() {
				if (hasNext()) {
					GServer ret = next;
					next = null;
					return ret;
				}
				throw new NoSuchElementException();
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	@Override
	public void move(final float dx, final float dy) {
		super.move(dx, dy);
		updateServersPositions();
	}

	/**
	 * put a server on a level, if it can.<br />
	 * It can if the specified level is empty, AND there are enough empty level
	 * on top of this to fit with the server's height
	 * 
	 * @param server
	 *            the server to put
	 * @param lvl
	 *            the level in the rack we want to put the server in
	 * @return the success of the operation.
	 */
	public boolean putServer(final GServer server, final int lvl) {
		final int maxServerLvl = server.getVSize() + lvl - 1;
		if (lvl < 0 || maxServerLvl >= getLvlNb()) {
			return false;
		}
		for (int i = lvl; i <= maxServerLvl; i++) {
			if (serverAtLvl(i) != null) {
				return false;
			}
		}
		for (int i = lvl; i <= maxServerLvl; i++) {
			forceServer(server, i);
		}
		updateServerPosition(server, lvl);
		return true;
	}

	/**
	 * try to place a server on a series of level, specified by first level and
	 * step on fail
	 * 
	 * @return -1 if the server could not be added, the level it was added on
	 *         success
	 */
	protected int sequentialPlaceServer(final GServer server,
			final int firstIndex, final int step) {
		// System.out.println("   sequentialPlaceServer " + server.getName()
		// + " , index " + firstIndex + ", step " + step);
		for (int i = firstIndex; i > -1 && i + server.getVSize() < getLvlNb(); i += step) {
			if (putServer(server, i)) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * @return the server positioned at level i
	 * @param i
	 *            the level in the server : From 0 to {@link #lvlNb}-1.
	 * @throws ArrayIndexOutOfBoundsException
	 *             if i is not a valid level
	 */
	public GServer serverAtLvl(final int i) {
		return servers[i];
	}

	@Override
	public void setAngle(float angle) {
		super.setAngle(angle);
		updateServersPositions();
	}

	@Override
	public void setElevation(final float elevation) {
		super.setElevation(elevation);
		updateServersPositions();
	}

	@Override
	public void setX(final float x) {
		super.setX(x);
		updateServersPositions();
	}

	@Override
	public void setY(final float y) {
		super.setY(y);
		updateServersPositions();
	}

	/**
	 * remove the server from the rack memory ; the server may still consider
	 * its owner to be this rack though
	 */
	public void unsetServer(final GServer server) {
		for (int i = 0; i < getLvlNb(); i++) {
			if (serverAtLvl(i) == server) {
				servers[i] = null;
			}
		}
	}

	protected void updateServerPosition(final GServer server, final int lvl) {
		server.setAngle(getAngle());
		server.setXWithinRack(getX());
		server.setYWithinRack(getY());
		server.setElevationWithinRack(border_bottom + lvl * Utils.U_IN_CM);
	}

	/**
	 * update the position of the server at given lvl to fit this' present
	 * position
	 */
	protected void updateServerPosition(final int lvl) {
		final GServer server = serverAtLvl(lvl);
		if (server == null) {
			return;
		}
		this.updateServerPosition(server, lvl);
	}

	public void updateServersPositions() {
		// System.out.println("updating the rack " + getName() + "'s servers");
		for (int i = 0; i < getLvlNb(); i++) {
			final GServer server = serverAtLvl(i);
			if (server != null) {
				// System.out.println("server " + server + " is at pos " + i);
				this.updateServerPosition(server, i);
				i += server.getVSize() - 1;
			}
		}
	}

	/**
	 * propagate the position of the graphical rack on the {@link Rack} it
	 * represents
	 * 
	 * @throws RemoteException
	 */
	protected void propagatePosition() throws RemoteException {
		gridElement.setPosX(getX());
		gridElement.setPosY(getY());
		gridElement.setElevation(getElevation());
		gridElement.setAngle(getAngle());
	}

	/**
	 * creates a {@link Level} for specified level number, or if one already
	 * exists, get it.
	 * 
	 * @param lvl
	 *            the number number of the required level
	 * @return a Level with corresponding level number.
	 * @throws RemoteException
	 */
	protected Level getLevel(int lvl) throws RemoteException {
		lvl += 1; // Gridlib rack levels start at 1, while GRacks level start at
		// 0
		for (Level level : gridElement.getLevels()) {
			if (level.getNumber() == lvl) {
				return level;
			}
		}
		try {
			Double number = Double.valueOf(lvl);
			Level ret = bridge.geFactory.newLevel(number);
			gridElement.addLevel(ret);
			return ret;
		} catch (ConfigurationException e) {
			throw new RuntimeException("TODO: Handle this exception better", e);
		}
	}

	protected void propagateServers() throws RemoteException {
		GServer lastServer = null;
		for (int lvl = 0; lvl < getLvlNb(); lvl++) {
			GServer server = serverAtLvl(lvl);
			if (server != lastServer && server != null) {
				try {
					Level level = getLevel(lvl);
					Machine gridMachine = server.getGridElement();
					Level ancientLevel = gridMachine.getLevel();
					if (level != ancientLevel) {
						if (ancientLevel != null) {
							ancientLevel.removeMachine();
						}
						level.addMachine(server.getGridElement());
					}
				} catch (ConfigurationException e) {
					e.printStackTrace();
				}
			}
			lastServer = server;
		}
	}

	/**
	 * ensure each internal server is contained in one of this' levels.
	 */
	@Override
	public void propagate() {
		try {
			propagatePosition();
			propagateServers();
			removeEmptyLevels();
		} catch (RemoteException e) {
			throw new RuntimeException("TODO: Handle this exception better", e);
		}
	}

	/**
	 * remove the empty levels in the {@link Rack} it refers to.
	 * 
	 * @throws RemoteException
	 */
	public void removeEmptyLevels() throws RemoteException {
		for (Iterator<Level> it = gridElement.getLevels().iterator(); it
				.hasNext();) {
			Level level = it.next();
			if (level.getMachine() == null) {
				System.out.println(" removing lvl " + level.getNumber()
						+ " from rack " + getName());
				it.remove();
			}
		}
	}

	@Override
	public void removeGridReferences() {
		try {
			bridge.geFactory.removeFromGlobals(getGridElement());
		} catch (RemoteException e) {
			e.printStackTrace();
		} catch (ConfigurationException e) {
			e.printStackTrace();
		}
	}

	protected Rack getGridElement() {
		return gridElement;
	}

	@Override
	public void graphicalDelete() {
		removeGridReferences();
		for (GServer server : servers) {
			if (server != null) {
				server.removeFromRack();
			}
		}
		bridge.gracks.remove(this);
	}

	public void renameServers() {
		GServer lastServer = null;
		for (int i = 0; i < servers.length; i++) {
			GServer server = servers[i];
			if (lastServer != server && server != null) {
				server.setName(getName() + "_s" + i);
			}
			lastServer = server;
		}
	}

}
